package com.woniu.controller;

import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.woniu.entity.AllUser;
import com.woniu.entity.dto.LoginDTO;
import com.woniu.entity.vo.AllUserVO;
import com.woniu.service.AllUserService;
import com.woniu.utils.TokenUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Map;

/**
 * @Author pml
 * @Date 2025/6/13 10:52
 * 登录控制器（WebFlux版本）
 * 为什么所有接口返回Mono？保证响应式链路，避免上下文丢失
 */
@RestController
@RequestMapping("/")
public class LoginController {

    @Autowired
    private AllUserService allUserService;
    @Autowired
    private TokenUtils tokenUtils;

    /**
     * 用户登录接口（返回双令牌）
     * 为什么用LoginDTO代替AllUser？
     * 1. 解决实体类无rememberMe字段的问题
     * 2. 明确区分视图层参数与数据库实体
     * 3. 便于添加参数校验（如@NotBlank）
     */
    @PostMapping("login")
    public Mono<SaResult> login(@RequestBody LoginDTO loginDTO, ServerWebExchange exchange) {  // 参数类型改为LoginDTO,添加ServerWebExchange参数
        // 查询用户信息（业务逻辑不变，仅参数来源变化）
        AllUser loginUser = allUserService.getOne(new QueryWrapper<AllUser>()
                .eq("user_name", loginDTO.getUserName()));

        // 用户名密码校验（业务逻辑不变）
        if (loginUser == null || !loginUser.getPassword().equals(loginDTO.getPassword())) {
            return Mono.just(SaResult.error("用户名或密码错误"));
        }

        // 根据"记住我"选项动态计算刷新令牌过期时间
        // 为什么分两种时长？
        // 1. 未勾选：1小时过期（平衡安全性与用户体验）
        // 2. 勾选：30天过期（减少频繁登录，适合信任设备）
        AllUserVO allUserVO = new AllUserVO();
        BeanUtils.copyProperties(loginUser, allUserVO);
        long refreshTokenTimeout = loginDTO.getRememberMe() ?
                30L * 24 * 60 * 60 :  // 30天 = 30*24*60*60秒
                3L * 60;             // 1小时 = 60*60秒，测试改成了3分钟3*60

        // 生成双令牌并返回（传递动态过期时间）
        return tokenUtils.generateDoubleToken(loginUser.getUserId(), refreshTokenTimeout)
                .map(tokenMap -> {
                    System.out.println(tokenMap);
                    allUserVO.setAccessToken(tokenMap.get("accessToken"));
                    allUserVO.setRefreshToken(tokenMap.get("refreshToken"));
                    return SaResult.ok().setMsg("登录成功").setData(allUserVO);
                });
    }

    /**
     * 刷新访问令牌接口
     * 为什么用Mono？保证响应式链路
     */
    @PostMapping("refreshToken")
    public Mono<SaResult> refreshToken(@RequestBody Map<String, String> request) {
        // 使用Map接收JSON请求体，提取refreshToken字段
        // 为什么不直接用String接收？因为前端发送的是JSON对象而非纯字符串，直接接收会导致整个JSON被当作令牌值
        String refreshToken = request.get("refreshToken");
        return tokenUtils.refreshAccessToken(refreshToken);
    }

    /**
     * 用户登出接口（同时使访问令牌和刷新令牌失效）
     * 为什么用Mono？保证响应式链路
     */
    @PostMapping("logout")
    public Mono<SaResult> logout(@RequestBody Map<String, String> request) {
        // 从请求中获取refreshToken
        // 为什么需要refreshToken参数？因为需要显式使Redis中的刷新令牌失效
        String refreshToken = request.get("refreshToken");

        // 1. 使刷新令牌在Redis中失效
        // 为什么先处理刷新令牌？防止登出过程中并发获取新令牌
        return tokenUtils.invalidateRefreshToken(refreshToken)
                .then(Mono.defer(() -> {
                    // 2. 使访问令牌失效（清除当前会话）
                    // 为什么用then()链式调用？确保刷新令牌失效后再处理访问令牌
                    StpUtil.logout();
                    // 3. 返回登出成功结果
                    return Mono.just(SaResult.ok().setMsg("登出成功"));
                }))
                // 错误处理：即使令牌已失效也返回成功，避免用户感知异常
                .onErrorResume(e -> Mono.just(SaResult.ok().setMsg("登出成功")));
    }

    /**
     * 测试接口：需要登录才能访问
     * 为什么用Mono？便于响应式链路测试上下文
     */
    @GetMapping("testContext")
    public Mono<SaResult> testContext() {
        return Mono.deferContextual(ctx -> {
            boolean hasContext = StpUtil.getTokenInfo() != null;
            return Mono.just(SaResult.ok().setData("上下文状态: " + (hasContext ? "正常" : "异常")));
        });
    }

    /**
     * 测试接口：需要管理员角色才能访问
     * 为什么用Mono？便于响应式链路权限测试
     */
    @SaCheckRole("ROLE_1")
    @GetMapping("admin")
    public Mono<SaResult> adminOnly() {
        return Mono.just(SaResult.ok("只有管理员可以访问"));
    }

    /**
     * 获取当前登录用户信息
     * 为什么用Mono？便于响应式链路获取用户信息
     */
    @GetMapping("currentUser")
    public Mono<SaResult> getCurrentUser() {
        return Mono.deferContextual(ctx -> {
            Integer userId = StpUtil.getLoginIdAsInt();
            AllUser user = allUserService.getById(userId);
            if (user == null) {
                return Mono.just(SaResult.error("用户不存在"));
            }
            user.setPassword(null);
            return Mono.just(SaResult.ok().setData(user));
        });
    }
}